大家好!經過昨天的努力,我們的「省錢拍拍」App 已經擁有一個可以流暢滾動的動態列表了。功能上邁進了一大步,但視覺上,它看起來仍像一個「樣板 App」——滿滿的預設色彩,缺乏品牌個性。
如果我們手動去修改每一個 Text
的顏色、每一個 Icon
的大小,那將會是一場災難。幸運的是,Flutter 提供了一套強大的中央樣式系統:ThemeData
。
學習如何從一個「種子顏色」生成和諧的色彩系統,並使用 google_fonts
套件,無需下載任何字體檔,就能為 App 打造一套獨一無二、貫穿全局的視覺主題。
ThemeData
是我們 App 全局樣式的設定檔。它就位於 lib/main.dart
中,MyApp Widget 的 build
方法裡,作為 MaterialApp
的 theme
屬性。
// lib/main.dart -> MyApp -> build
@override
Widget build(BuildContext context) {
return MaterialApp(
// ...
// 就是它!App 的風格都由這裡定義
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: HomePage(),
);
}
目前,它只用了一行 ColorScheme.fromSeed(seedColor: Colors.deepPurple) 來定義顏色。這就是我們著手改造的起點。
在 Material 3 的設計規範中,App 的色彩由一組語意化的 ColorScheme
(色彩方案) 驅動。它包含 primary
(主要色)、secondary
(次要色)、surface
(表面色)、background
(背景色) 等。
而 ColorScheme.fromSeed()
是一個非常聰明的建構子:你只需要給它一個「種子顏色」,Flutter 就會自動為你生成一整套完整、和諧、且包含淺色與深色模式的 ColorScheme
。
我們希望「省錢拍拍」App 的主題色能給人一種清新、穩定的感覺,因此選擇綠色系的 Colors.teal
作為種子顏色。
修改 ThemeData:
// lib/main.dart -> MyApp -> build -> theme
theme: ThemeData(
// 只需要改變這個種子顏色
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal), // 從 deepPurple 改為 teal
useMaterial3: true,
),
Hot Restart 後,App 的主色調就會立刻更新。
過去,在 Flutter 中使用自訂字體需要手動下載字體檔、建立資料夾、並在 pubspec.yaml
中註冊,過程繁瑣。現在,有了 google_fonts
套件,一切都變得無比簡單。
google_fonts
依賴pubspec.yaml
檔案,在 dependencies
區塊下,加入 google_fonts
:# pubspec.yaml
dependencies:
flutter:
sdk: flutter
# ... 其他套件 ...
# 新增這一行
google_fonts: ^6.3.1 # 建議使用最新版本
修改完 pubspec.yaml
後,VS Code 通常會自動執行 flutter pub get
。如果沒有,請手動在終端機執行它,或者點擊右上角的 "Get Packages" 按鈕。
lib/main.dart
首先在文件頂部導入 google_fonts
套件:// lib/main.dart
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; // 導入 google_fonts
接著,修改 ThemeData
。我們不再使用 fontFamily
屬性,而是直接將 textTheme
替換為 google_fonts
提供的版本。這裡我們選用 Noto Sans TC (思源黑體-繁體中文)。
// lib/main.dart -> MyApp -> build -> theme
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
useMaterial3: true,
// 舊方法是使用 fontFamily,現在我們用這個更強大的方式
textTheme: GoogleFonts.notoSansTCTextTheme(
Theme.of(context).textTheme,
),
),
程式碼解析:
GoogleFonts.notoSansTCTextTheme()
: 這是套件提供的輔助函式,它會自動從網路下載 Noto Sans TC 字體(並在設備上快取),然後建立一個完整的 TextTheme
(包含 headline
, body
, title
等所有文字樣式)。Theme.of(context).textTheme
: 我們將 App 的原始 textTheme
傳入,google_fonts
會聰明地將新字體應用在原始的樣式設定上(例如顏色、大小),而不是完全覆蓋。情況一:全局樣式已滿足需求
當我們嘗試讓首頁總支出金額的文字變大、更突出時,可能會遇到一個編譯錯誤。如果你直接在 const Column
內使用 Theme.of(context)
,編譯器會報錯。
// lib/main.dart -> HomePage -> build -> 總覽區塊
// ...
Text(
'NT\$ 12,345',
// 這裡的 headlineLarge 樣式已經自動套用了 Noto Sans TC 字體
style: Theme.of(context).textTheme.headlineLarge,
),
// ...
const Column(...)
,等於在告訴編譯器,這個 Column
及其所有子元件的內容在編譯時就已經完全確定,程式運行時無需再重新計算,可以進行效能優化。Theme.of(context)
:這是一個運行時的方法。它會沿著 Widget 樹向上查找當前 context
下的 ThemeData。因為 context
本身就是一個運行時的物件,所以這個操作的結果不可能是編譯時期的常數。const Column
的子元件 Text
中,使用了 Theme.of(context)
這個運行時的值,就破壞了 const
的約定。修改 HomePage
的 build
方法中的總覽區塊:
// lib/main.dart -> HomePage -> build -> 總覽區塊
// ...
Container(
padding: const EdgeInsets.all(24.0),
margin: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(10),
),
// 【重要觀念修正】: 因為下方的 Text Widget 使用了 Theme.of(context),
// 它是一個執行時的值,所以這裡的 Column 不能是 const。
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'本月總支出',
style: TextStyle(fontSize: 16, color: Colors.black54),
),
Text(
'NT\$ 12,345',
// 使用 Theme.of(context) 來取得當前主題的樣式
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
color: Colors.teal.shade700, // 我們還可以在獲取的基礎上微調
),
),
],
),
),
// ...
情況二:需要單獨使用特定字體或樣式
假設在某個特殊頁面,你想使用不同的字體,或者只是想微調某個 Text
的樣式,也可以直接呼叫 GoogleFonts
。
// 一個單獨使用的範例
Text(
'特殊標語',
style: GoogleFonts.lato( // 直接指定使用 Lato 字體
fontSize: 24,
fontStyle: FontStyle.italic,
color: Colors.pink,
),
),
今天我們為「省錢拍拍」進行了一次徹底且現代化的「形象改造」。學會了:
ColorScheme.fromSeed
快速建立一套和諧的色彩系統。google_fonts
套件,無需管理字體檔,就能優雅地為 App 設定全局字體。textTheme
) 與局部設定 (GoogleFonts.lato()
) 的使用時機。我們的 App 現在外觀與功能兼備,但它仍然是一個只能「看」的 App。從明天開始,我們將正式進入「互動」的領域:學習如何處理使用者輸入,並打造新增消費紀錄的表單頁面。